﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Configuration;
using System.Web.Hosting;
using System.Web.SessionState;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Builders;

namespace PaceR.Mongo
{
	public class MongoSessionStateProvider : SessionStateStoreProviderBase
	{
		private MongoCollection _mongoCollection;
		private SessionStateSection _sessionStateSection;

		public override void Initialize(string name, NameValueCollection config)
		{
			var configuration = WebConfigurationManager.OpenWebConfiguration(HostingEnvironment.ApplicationVirtualPath);
			this._sessionStateSection = configuration.GetSection("system.web/sessionState") as SessionStateSection;

			var client = new MongoClient(config["connectionString"] ?? "mongodb://localhost");
			this._mongoCollection = client.GetServer().GetDatabase(config["database"] ?? "ASPNETDB").GetCollection(config["collection"] ?? "SessionState");
			this._mongoCollection.EnsureIndex("applicationVirtualPath", "id", "lockId");

			//clean any old sessions that may have been orphaned after an app restart
			ClearExpiredSessions();
			base.Initialize(name, config);
		}

		public override SessionStateStoreData CreateNewStoreData(System.Web.HttpContext context, int timeout)
		{
			return new SessionStateStoreData(new SessionStateItemCollection(), SessionStateUtility.GetSessionStaticObjects(context), timeout);
		}

		public override void CreateUninitializedItem(System.Web.HttpContext context, string id, int timeout)
		{
			using (var memoryStream = new MemoryStream())
			{
				using (var binaryWriter = new BinaryWriter(memoryStream))
				{
					CreateNewSessionItem(memoryStream, id, CreateNewStoreData(context, timeout));
				}
			}
		}

		public override void Dispose()
		{ }

		public override void EndRequest(System.Web.HttpContext context)
		{ }

		public override SessionStateStoreData GetItem(System.Web.HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
		{
			return GetSessionStateStoreData(false, context, id, out locked, out lockAge, out lockId, out actions);
		}

		public override SessionStateStoreData GetItemExclusive(System.Web.HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
		{
			return GetSessionStateStoreData(true, context, id, out locked, out lockAge, out lockId, out actions);
		}

		public override void InitializeRequest(System.Web.HttpContext context)
		{ }

		public override void ReleaseItemExclusive(System.Web.HttpContext context, string id, object lockId)
		{
			var query = Query.And(Query.EQ("applicationVirtualPath", HostingEnvironment.ApplicationVirtualPath), Query.EQ("id", id), Query.EQ("lockId", BsonValue.Create(lockId)));
			var update = Update.Set("expires", DateTime.Now.Add(_sessionStateSection.Timeout)).Set("locked", false);
			this._mongoCollection.Update(query, update);
		}

		public override void RemoveItem(System.Web.HttpContext context, string id, object lockId, SessionStateStoreData item)
		{
			var query = Query.And(Query.EQ("applicationVirtualPath", HostingEnvironment.ApplicationVirtualPath), Query.EQ("id", id), Query.EQ("lockId", BsonValue.Create(lockId)));
			this._mongoCollection.Remove(query);
		}

		public override void ResetItemTimeout(System.Web.HttpContext context, string id)
		{
			var query = Query.And(Query.EQ("applicationVirtualPath", HostingEnvironment.ApplicationVirtualPath), Query.EQ("id", id));
			var update = Update.Set("expires", DateTime.Now.Add(_sessionStateSection.Timeout));
			this._mongoCollection.Update(query, update);
		}

		public override void SetAndReleaseItemExclusive(System.Web.HttpContext context, string id, SessionStateStoreData item, object lockId, bool newItem)
		{
			using (var memoryStream = new MemoryStream())
			{
				using (var binaryWriter = new BinaryWriter(memoryStream))
				{
					((SessionStateItemCollection)item.Items).Serialize(binaryWriter);

					if (newItem)
					{
						var query = Query.And(Query.EQ("applicationVirtualPath", HostingEnvironment.ApplicationVirtualPath), Query.EQ("id", id));
						this._mongoCollection.Remove(query);

						CreateNewSessionItem(memoryStream, id, item);
					}
					else
					{
						var query = Query.And(Query.EQ("applicationVirtualPath", HostingEnvironment.ApplicationVirtualPath), Query.EQ("id", id), Query.EQ("lockId", BsonValue.Create(lockId)));
						var update = Update.Set("expires", DateTime.Now.Add(this._sessionStateSection.Timeout)).Set("sessionStateItems", memoryStream.ToArray()).Set("locked", false).Set("sessionStateItemsCount", item.Items.Count);
						this._mongoCollection.Update(query, update);
					}
				}
			}
		}

		public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback)
		{
			return false;
		}

		#region Private Methods
		private void CreateNewSessionItem(MemoryStream memoryStream, string id, SessionStateStoreData item)
		{
			var bsonDocument = new BsonDocument
            {
                { "applicationVirtualPath", HostingEnvironment.ApplicationVirtualPath },
                { "created", DateTime.Now },
                { "expires", DateTime.Now.AddMinutes(item.Timeout) },
                { "id", id },
                { "lockDate", DateTime.Now },
                { "locked", false },
                { "lockId", 0 },
                { "sessionStateActions", SessionStateActions.None },
                { "sessionStateItems", memoryStream.ToArray() },
                { "sessionStateItemsCount", item.Items.Count },
                { "timeout", item.Timeout }
            };

			this._mongoCollection.Insert(bsonDocument);
		}

		private SessionStateStoreData GetSessionStateStoreData(bool exclusive, System.Web.HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
		{
			actions = SessionStateActions.None;
			lockAge = TimeSpan.Zero;
			locked = false;
			lockId = null;

			var query = Query.And(Query.EQ("applicationVirtualPath", HostingEnvironment.ApplicationVirtualPath), Query.EQ("id", id));
			var bsonDocument = this._mongoCollection.FindOneAs<BsonDocument>(query);

			if (bsonDocument == null)
			{
				locked = false;
				return null;
			}
			else if (bsonDocument["expires"].ToUniversalTime() <= DateTime.Now)
			{
				locked = false;
				this._mongoCollection.Remove(Query.And(Query.EQ("applicationVirtualPath", HostingEnvironment.ApplicationVirtualPath), Query.EQ("id", id)));
			}
			else if (bsonDocument["locked"].AsBoolean == true)
			{
				lockAge = DateTime.Now.Subtract(bsonDocument["lockDate"].ToUniversalTime());
				locked = true;
				lockId = bsonDocument["lockId"].AsInt32;
			}
			else
			{
				locked = false;
				lockId = bsonDocument["lockId"].AsInt32;
				actions = (SessionStateActions)bsonDocument["sessionStateActions"].AsInt32;
			}

			if (exclusive)
			{
				lockId = (int)(lockId ?? 0) + 1;
				actions = SessionStateActions.None;

				var update = Update.Set("lockDate", DateTime.Now).Set("lockId", (int)lockId).Set("locked", true).Set("sessionStateActions", SessionStateActions.None);
				this._mongoCollection.Update(query, update);
			}

			if (actions == SessionStateActions.InitializeItem)
			{
				return CreateNewStoreData(context, _sessionStateSection.Timeout.Minutes);
			}

			using (var memoryStream = new MemoryStream(bsonDocument["sessionStateItems"].AsByteArray))
			{
				var sessionStateItems = new SessionStateItemCollection();

				if (memoryStream.Length > 0)
				{
					var binaryReader = new BinaryReader(memoryStream);
					sessionStateItems = SessionStateItemCollection.Deserialize(binaryReader);
				}

				return new SessionStateStoreData(sessionStateItems, SessionStateUtility.GetSessionStaticObjects(context), bsonDocument["timeout"].AsInt32);
			}
		}

		private void ClearExpiredSessions()
		{
			this._mongoCollection.Remove(Query.And(Query.EQ("applicationVirtualPath", HostingEnvironment.ApplicationVirtualPath), Query.LT("expires", DateTime.Now)));
		}
		#endregion
	}
}
